////////////////////////////////////

/**
* Intercooler.js - there is no need to be upset.
*/
var Intercooler = Intercooler || (function () {
  'use strict'; // inside function for better merging

  //--------------------------------------------------
  // Vars
  //--------------------------------------------------
  var _MACROS = ['ic-get-from', 'ic-post-to', 'ic-put-to', 'ic-patch-to', 'ic-delete-from',
    'ic-style-src', 'ic-attr-src', 'ic-prepend-from', 'ic-append-from'];
  var _scrollHandler = null;
  var _UUID = 1;
  var _readyHandlers = [];

  //============================================================
  // Base Swap Definitions
  //============================================================
  function remove(elt) {
    elt.remove();
  }

  function showIndicator(elt) {
    if (elt.closest('.ic-use-transition').length > 0) {
      elt.data('ic-use-transition', true);
      elt.removeClass('ic-use-transition');
    } else {
      elt.show();
    }
  }

  function hideIndicator(elt) {
    if (elt.data('ic-use-transition')) {
      elt.data('ic-use-transition', null);
      elt.addClass('ic-use-transition');
    } else {
      elt.hide();
    }
  }

  function prepend(parent, responseContent) {
    try {
      parent.prepend(responseContent);
    } catch (e) {
      log(elt, formatError(e), "ERROR");
    }
    if (parent.attr('ic-limit-children')) {
      var limit = parseInt(parent.attr('ic-limit-children'));
      if (parent.children().length > limit) {
        parent.children().slice(limit, parent.children().length).remove();
      }
    }
  }

  function append(parent, responseContent) {
    try {
      parent.append(responseContent);
    } catch (e) {
      log(elt, formatError(e), "ERROR");
    }
    if (parent.attr('ic-limit-children')) {
      var limit = parseInt(parent.attr('ic-limit-children'));
      if (parent.children().length > limit) {
        parent.children().slice(0, parent.children().length - limit).remove();
      }
    }
  }

  //============================================================
  // Utility Methods
  //============================================================
  function log(elt, msg, level) {
    if (elt == null) {
      elt = $('body');
    }
    elt.trigger("log.ic", [msg, level, elt]);
    if (level == "ERROR") {
      if (window.console) {
        window.console.log("Intercooler Error : " + msg);
      }
      var errorUrl = closestAttrValue($('body'), 'ic-post-errors-to');
      if (errorUrl) {
        $.post(errorUrl, {'error': msg})
      }
    }
  }

  function uuid() {
    return _UUID++;
  }

  function icSelectorFor(elt) {
    return "[ic-id='" + getIntercoolerId(elt) + "']";
  }

  function parseInterval(str) {
    log(null, "POLL: Parsing interval string " + str, 'DEBUG');
    if (str == "null" || str == "false" || str == "") {
      return null;
    } else if (str.lastIndexOf("ms") == str.length - 2) {
      return parseFloat(str.substr(0, str.length - 2));
    } else if (str.lastIndexOf("s") == str.length - 1) {
      return parseFloat(str.substr(0, str.length - 1)) * 1000;
    } else {
      return 1000;
    }
  }

  function initScrollHandler() {
    if (_scrollHandler == null) {
      _scrollHandler = function () {
        $("[ic-trigger-on='scrolled-into-view']").each(function () {
          if (isScrolledIntoView($(this)) && $(this).data('ic-scrolled-into-view-loaded') != true) {
            $(this).data('ic-scrolled-into-view-loaded', true);
            fireICRequest($(this));
          }
        })
      };
      $(window).scroll(_scrollHandler);
    }
  }

  function currentUrl() {
    return window.location.pathname + window.location.search + window.location.hash;
  }

  // taken from turbolinks.js
  function createDocument(html) {
    var doc = null;
    if (/<(html|body)/i.test(html)) {
      doc = document.documentElement.cloneNode();
      doc.innerHTML = html;
    } else {
      doc = document.documentElement.cloneNode(true);
      doc.querySelector('body').innerHTML = html;
    }
    return $(doc);
  }

  //============================================================
  // Request/Parameter/Include Processing
  //============================================================
  function getTarget(elt) {
    var closest = $(elt).closest('[ic-target]');
    var targetValue = closest.attr('ic-target');
    if (targetValue == 'this') {
      return closest;
    } else if (targetValue && targetValue.indexOf('this.') != 0) {
      if (targetValue.indexOf('closest ') == 0) {
        return elt.closest(targetValue.substr(8));
      } else {
        return $(targetValue);
      }
    } else {
      return elt;
    }
  }

  function processHeaders(elt, xhr) {

    elt.trigger("beforeHeaders.ic", [elt, xhr]);
    log(elt, "response headers: " + xhr.getAllResponseHeaders(), "DEBUG");
    var target = null;

    if (xhr.getResponseHeader("X-IC-Refresh")) {
      var pathsToRefresh = xhr.getResponseHeader("X-IC-Refresh").split(",");
      log(elt, "X-IC-Refresh: refreshing " + pathsToRefresh, "DEBUG");
      $.each(pathsToRefresh, function (i, str) {
        refreshDependencies(str.replace(/ /g, ""), elt);
      });
    }

    if (xhr.getResponseHeader("X-IC-Script")) {
      log(elt, "X-IC-Script: evaling " + xhr.getResponseHeader("X-IC-Script"), "DEBUG");
      eval(xhr.getResponseHeader("X-IC-Script"));
    }

    if (xhr.getResponseHeader("X-IC-Redirect")) {
      log(elt, "X-IC-Redirect: redirecting to " + xhr.getResponseHeader("X-IC-Redirect"), "DEBUG");
      window.location = xhr.getResponseHeader("X-IC-Redirect");
    }

    if (xhr.getResponseHeader("X-IC-CancelPolling") == "true") {
      cancelPolling($(elt).closest('[ic-poll]'));
    }

    if (xhr.getResponseHeader("X-IC-ResumePolling") == "true") {
      var pollingElt = $(elt).closest('[ic-poll]');
      pollingElt.attr('ic-pause-polling', null);
      startPolling(pollingElt);
    }

    if (xhr.getResponseHeader("X-IC-Open")) {
      log(elt, "X-IC-Open: opening " + xhr.getResponseHeader("X-IC-Open"), "DEBUG");
      window.open(xhr.getResponseHeader("X-IC-Open"));
    }

    var triggerValue = xhr.getResponseHeader("X-IC-Trigger");
    if (triggerValue) {
      log(elt, "X-IC-Trigger: found trigger " + triggerValue, "DEBUG");
      target = getTarget(elt);
      // Deprecated API
      if (xhr.getResponseHeader("X-IC-Trigger-Data")) {
        var triggerArgs = $.parseJSON(xhr.getResponseHeader("X-IC-Trigger-Data"));
        target.trigger(triggerValue, triggerArgs);
      } else {
        if(triggerValue.indexOf("{") >= 0) {
          $.each($.parseJSON(triggerValue), function(event, args) {
            target.trigger(event, args);
          });
        } else {
          target.trigger(triggerValue, []);
        }
      }
    }

    if (xhr.getResponseHeader("X-IC-Remove")) {
      if (elt) {
        target = getTarget(elt);
        log(elt, "X-IC-Remove header found.", "DEBUG");
        remove(target);
      }
    }

    elt.trigger("afterHeaders.ic", [elt, xhr]);

    return true;
  }


  function beforeRequest(elt) {
    elt.addClass('disabled');
    elt.data('ic-request-in-flight', true);
  }

  function requestCleanup(indicator, elt) {
    if (indicator.length > 0) {
      hideIndicator(indicator);
    }
    elt.removeClass('disabled');
    elt.data('ic-request-in-flight', false);
    if (elt.data('ic-next-request')) {
      elt.data('ic-next-request')();
      elt.data('ic-next-request', null);
    }
  }

  function replaceOrAddMethod(data, actualMethod) {
    var regex = /(&|^)_method=[^&]*/;
    var content = "&_method=" + actualMethod;
    if (regex.test(data)) {
      return data.replace(regex, content)
    } else {
      return data + "&" + content;
    }
  }

  function globalEval(script) {
    return window[ "eval" ].call(window, script);
  }

  function closestAttrValue(elt, attr) {
    var closestElt = $(elt).closest('[' + attr + ']');
    if (closestElt.length > 0) {
      return closestElt.attr(attr);
    } else {
      return null;
    }
  }

  function formatError(e) {
    var msg = e.toString() + "\n";
    try {
      msg += e.stack;
    } catch (e) {
      // ignore
    }
    return msg;
  }

  function handleRemoteRequest(elt, type, url, data, success) {

    beforeRequest(elt);

    data = replaceOrAddMethod(data, type);

    // Spinner support
    var indicator = findIndicator(elt);
    if (indicator.length > 0) {
      showIndicator(indicator);
    }

    var requestId = uuid();
    var requestStart = new Date();
    var actualRequestType = type == 'GET' ? 'GET' : 'POST';

    $.ajax({
      type: actualRequestType,
      url: url,
      data: data,
      dataType: 'text',
      headers: {
        "Accept": "text/html-partial, */*; q=0.9",
        "X-IC-Request": true,
        "X-HTTP-Method-Override": type
      },
      beforeSend: function (xhr, settings) {
        elt.trigger("beforeSend.ic", [elt, data, settings, xhr, requestId]);
        log(elt, "before AJAX request " + requestId + ": " + type + " to " + url, "DEBUG");
        var onBeforeSend = closestAttrValue(elt, 'ic-on-beforeSend');
        if (onBeforeSend) {
          globalEval('(function (data, settings, xhr) {' + onBeforeSend + '})')(data, settings, xhr);
        }
      },
      success: function (data, textStatus, xhr) {
        elt.trigger("success.ic", [elt, data, textStatus, xhr, requestId]);
        log(elt, "AJAX request " + requestId + " was successful.", "DEBUG");
        var onSuccess = closestAttrValue(elt, 'ic-on-success');
        if (onSuccess) {
          if (globalEval('(function (data, textStatus, xhr) {' + onSuccess + '})')(data, textStatus, xhr) == false) {
            return;
          }
        }

        var beforeHeaders = new Date();
        try {
          if (processHeaders(elt, xhr)) {
            log(elt, "Processed headers for request " + requestId + " in " + (new Date() - beforeHeaders) + "ms", "DEBUG");
            var beforeSuccess = new Date();

            if (xhr.getResponseHeader("X-IC-PushURL") || closestAttrValue(elt, 'ic-push-url') == "true") {
              try {
                requestCleanup(indicator, elt); // clean up before snap-shotting HTML
                var newUrl = xhr.getResponseHeader("X-IC-PushURL") || closestAttrValue(elt, 'ic-src');
                _history.snapshotForHistory(newUrl);
              } catch(e) {
                log(elt, "Error during history snapshot for " + requestId + ": " + formatError(e), "ERROR");
              }
            }

            success(data, textStatus, elt, xhr);

            log(elt, "Process content for request " + requestId + " in " + (new Date() - beforeSuccess) + "ms", "DEBUG");
          }
          elt.trigger("after.success.ic", [elt, data, textStatus, xhr, requestId]);
        } catch (e) {
          log(elt, "Error processing successful request " + requestId + " : " + formatError(e), "ERROR");
        }
      },
      error: function (xhr, status, str) {
        elt.trigger("error.ic", [elt, status, str, xhr]);
        var onError = closestAttrValue(elt, 'ic-on-error');
        if (onError) {
          globalEval('(function (status, str, xhr) {' + onError + '})')(status, str, xhr);
        }
        log(elt, "AJAX request " + requestId + " experienced an error: " + str, "ERROR");
      },
      complete: function (xhr, status) {
        log(elt, "AJAX request " + requestId + " completed in " + (new Date() - requestStart) + "ms", "DEBUG");
        requestCleanup(indicator, elt);
        try {
          if ($.contains(document, elt[0])) {
            $(elt).trigger("complete.ic", [elt, data, status, xhr, requestId]);
          } else {
            $('body').trigger("complete.ic", [elt, data, status, xhr, requestId]);
          }
        } catch (e) {
          log(elt, "Error during complete.ic event for " + requestId + " : " + formatError(e), "ERROR");
        }
        var onComplete = closestAttrValue(elt, 'ic-on-complete');
        if (onComplete) {
          globalEval('(function (xhr, status) {' + onComplete + '})')(xhr, status);
        }
      }
    })
  }

  function findIndicator(elt) {
    var indicator = null;
    if ($(elt).attr('ic-indicator')) {
      indicator = $($(elt).attr('ic-indicator')).first();
    } else {
      indicator = $(elt).find(".ic-indicator").first();
      if (indicator.length == 0) {
        var parent = closestAttrValue(elt, 'ic-indicator');
        if (parent) {
          indicator = $(parent).first();
        } else {
          if($(elt).next().is('.ic-indicator')) {
            indicator = $(elt).next();
          }
        }
      }
    }
    return indicator;
  }

  function processIncludes(str) {
    var returnString = "";
    if ($.trim(str).indexOf("{") == 0) {
      var obj = $.parseJSON(str);
      $.each(obj, function (key, value) {
        returnString += "&" + encodeURIComponent(key) + "=" + encodeURIComponent(value);
      });
    } else {
      $(str).each(function () {
        returnString += "&" + $(this).serialize();
      });
    }
    return returnString;
  }

  function getParametersForElement(verb, elt, triggerOrigin) {
    var target = getTarget(elt);
    var str = "ic-request=true";

    // if the element is in a form, include the entire form
    if (verb != "GET" && elt.closest('form').length > 0) {
      str += "&" + elt.closest('form').serialize();
    } else { // otherwise include the element
      str += "&" + elt.serialize();
    }

    var promptText = closestAttrValue(elt, 'ic-prompt');
    if(promptText) {
      var promptVal = prompt(promptText);
      str += "&ic-prompt-value=" + encodeURIComponent(promptVal);
    }

    if (elt.attr('id')) {
      str += "&ic-element-id=" + elt.attr('id');
    }
    if (elt.attr('name')) {
      str += "&ic-element-name=" + elt.attr('name');
    }
    if (target.attr('ic-id')) {
      str += "&ic-id=" + target.attr('ic-id');
    }
    if (target.attr('id')) {
      str += "&ic-target-id=" + target.attr('id');
    }
    if (triggerOrigin && triggerOrigin.attr('id')) {
      str += "&ic-trigger-id=" + triggerOrigin.attr('id');
    }
    if (triggerOrigin && triggerOrigin.attr('name')) {
      str += "&ic-trigger-name=" + triggerOrigin.attr('name');
    }
    if (target.data('ic-last-refresh')) {
      str += "&ic-last-refresh=" + target.data('ic-last-refresh');
    }
    var includeAttr = closestAttrValue(elt, 'ic-include');
    if (includeAttr) {
      str += processIncludes(includeAttr);
    }
    $('[ic-global-include]').each(function() {
      str += processIncludes($(this).attr('ic-global-include'));
    });
    str += "&ic-current-url=" + encodeURIComponent(currentUrl());
    log(elt, "request parameters " + str, "DEBUG");
    return str;
  }

  function maybeSetIntercoolerInfo(elt) {
    var target = getTarget(elt);
    getIntercoolerId(target);
    maybeSetIntercoolerMetadata(target);
    if (elt.data('elementAdded.ic') != true) {
      elt.data('elementAdded.ic', true);
      elt.trigger("elementAdded.ic");
    }
  }

  function updateIntercoolerMetaData(elt) {
    elt.data('ic-last-refresh', new Date().getTime());
  }

  function maybeSetIntercoolerMetadata(elt) {
    elt.data('ic-last-refresh', new Date().getTime());
  }

  function getIntercoolerId(elt) {
    if (!elt.attr('ic-id')) {
      elt.attr('ic-id', uuid());
    }
    return elt.attr('ic-id');
  }

  //============================================================
  // Tree Processing
  //============================================================

  function processNodes(elt) {
    if(elt.length > 1) {
      elt.each(function(){
        processNodes($(this));
      })
    } else {
      processMacros(elt);
      processSources(elt);
      processPolling(elt);
      processTriggerOn(elt);
      processRemoveAfter(elt);
      processAddClasses(elt);
      processRemoveClasses(elt);
    }
  }

  function fireReadyStuff(elt) {
    elt.trigger('nodesProcessed.ic');
    $.each(_readyHandlers, function (i, handler) {
      try {
        handler(elt);
      } catch (e) {
        log(elt, formatError(e), "ERROR");
      }
    });
  }

  function processMacros(elt) {
    $.each(_MACROS, function (i, macro) {
      if ($(elt).closest('.ic-ignore').length == 0) {
        if ($(elt).is('[' + macro + ']')) {
          processMacro(macro, $(elt));
        }
        $(elt).find('[' + macro + ']').each(function () {
          if ($(this).closest('.ic-ignore').length == 0) {
            processMacro(macro, $(this));
          }
        });
      }
    });
  }

  function processSources(elt) {
    if ($(elt).closest('.ic-ignore').length == 0) {
      if ($(elt).is("[ic-src]")) {
        maybeSetIntercoolerInfo($(elt));
      }
      $(elt).find("[ic-src]").each(function () {
        if ($(this).closest('.ic-ignore').length == 0) {
          maybeSetIntercoolerInfo($(this));
        }
      });
    }
  }

  function processPolling(elt) {
    if ($(elt).closest('.ic-ignore').length == 0) {
      if ($(elt).is('[ic-poll]')) {
        maybeSetIntercoolerInfo($(elt));
        startPolling(elt);
      }
      $(elt).find('[ic-poll]').each(function () {
        if ($(this).closest('.ic-ignore').length == 0) {
          maybeSetIntercoolerInfo($(this));
          startPolling($(this));
        }
      });
    }
  }

  function processTriggerOn(elt) {
    if ($(elt).closest('.ic-ignore').length == 0) {
      handleTriggerOn(elt);
      $(elt).find('[ic-trigger-on]').each(function () {
        if ($(this).closest('.ic-ignore').length == 0) {
          handleTriggerOn($(this));
        }
      });
    }
  }

  function processRemoveAfter(elt) {
    if ($(elt).closest('.ic-ignore').length == 0) {
      handleRemoveAfter(elt);
      $(elt).find('[ic-remove-after]').each(function () {
        if ($(this).closest('.ic-ignore').length == 0) {
          handleRemoveAfter($(this));
        }
      });
    }
  }

  function processAddClasses(elt) {
    if ($(elt).closest('.ic-ignore').length == 0) {
      handleAddClasses(elt);
      $(elt).find('[ic-add-class]').each(function () {
        if ($(this).closest('.ic-ignore').length == 0) {
          handleAddClasses($(this));
        }
      });
    }
  }

  function processRemoveClasses(elt) {
    if ($(elt).closest('.ic-ignore').length == 0) {
      handleRemoveClasses(elt);
      $(elt).find('[ic-remove-class]').each(function () {
        if ($(this).closest('.ic-ignore').length == 0) {
          handleRemoveClasses($(this));
        }
      });
    }
  }

  //============================================================
  // Polling support
  //============================================================

  function startPolling(elt) {
    if (elt.data('ic-poll-interval-id') == null && $(elt).attr('ic-pause-polling') != 'true') {
      var interval = parseInterval(elt.attr('ic-poll'));
      if (interval != null) {
        var selector = icSelectorFor(elt);
        var repeats = parseInt(elt.attr('ic-poll-repeats')) || -1;
        var currentIteration = 0;
        log(elt, "POLL: Starting poll for element " + selector, "DEBUG");
        var timerId = setInterval(function () {
          var target = $(selector);
          elt.trigger("onPoll.ic", target);
          if ((target.length == 0) || (currentIteration == repeats) || elt.data('ic-poll-interval-id') != timerId) {
            log(elt, "POLL: Clearing poll for element " + selector, "DEBUG");
            clearTimeout(timerId);
          } else {
            fireICRequest(target);
          }
          currentIteration++;
        }, interval);
        elt.data('ic-poll-interval-id', timerId);
      }
    }
  }

  function cancelPolling(elt) {
    if (elt.data('ic-poll-interval-id') != null) {
      clearTimeout(elt.data('ic-poll-interval-id'));
      elt.data('ic-poll-interval-id', null);
    }
  }

  //============================================================----
  // Dependency support
  //============================================================----

  function refreshDependencies(dest, src) {
    log(src, "refreshing dependencies for path " + dest, "DEBUG");
    $('[ic-src]').each(function () {
      var fired = false;
      if (verbFor($(this)) == "GET" && $(this).attr('ic-deps') != 'ignore' && typeof($(this).attr('ic-poll')) == 'undefined') {
        if (isDependent(dest, $(this).attr('ic-src'))) {
          if (src == null || $(src)[0] != $(this)[0]) {
            fireICRequest($(this));
            fired = true;
          }
        } else if (isDependent(dest, $(this).attr('ic-deps')) || $(this).attr('ic-deps') == "*") {
          if (src == null || $(src)[0] != $(this)[0]) {
            fireICRequest($(this));
            fired = true;
          }
        }
      }
      if (fired) {
        log($(this), "depends on path " + dest + ", refreshing...", "DEBUG")
      }
    });
  }

  function isDependent(src, dest) {
    return (src && dest) && (dest.indexOf(src) == 0 || src.indexOf(dest) == 0);
  }

  //============================================================----
  // Trigger-On support
  //============================================================----

  function verbFor(elt) {
    if (elt.attr('ic-verb')) {
      return elt.attr('ic-verb').toUpperCase();
    }
    return "GET";
  }

  function eventFor(attr, elt) {
    if (attr == "default") {
      if ($(elt).is('button')) {
        return 'click';
      } else if ($(elt).is('form')) {
        return 'submit';
      } else if ($(elt).is(':input')) {
        return 'change';
      } else {
        return 'click';
      }
    } else {
      return attr;
    }
  }

  function preventDefault(elt) {
    return elt.is('form') || (elt.is(':submit') && elt.closest('form').length == 1);
  }

  function handleRemoveAfter(elt) {
    if ($(elt).attr('ic-remove-after')) {
      var interval = parseInterval($(elt).attr('ic-remove-after'));
      setTimeout(function () {
        remove(elt);
      }, interval);
    }
  }

  function parseAndApplyClass(classInfo, elt, operation) {
    var cssClass = "";
    var delay = 50;
    if (classInfo.indexOf(":") > 0) {
      var split = classInfo.split(':');
      cssClass = split[0];
      delay = parseInterval(split[1]);
    } else {
      cssClass = classInfo;
    }
    setTimeout(function () {
      elt[operation](cssClass)
    }, delay);
  }

  function handleAddClasses(elt) {
    if ($(elt).attr('ic-add-class')) {
      var values = $(elt).attr('ic-add-class').split(",");
      var arrayLength = values.length;
      for (var i = 0; i < arrayLength; i++) {
        parseAndApplyClass($.trim(values[i]), elt, 'addClass');
      }
    }
  }

  function handleRemoveClasses(elt) {
    if ($(elt).attr('ic-remove-class')) {
      var values = $(elt).attr('ic-remove-class').split(",");
      var arrayLength = values.length;
      for (var i = 0; i < arrayLength; i++) {
        parseAndApplyClass($.trim(values[i]), elt, 'removeClass');
      }
    }
  }

  function handleTriggerOn(elt) {

    if ($(elt).attr('ic-trigger-on')) {
      if ($(elt).attr('ic-trigger-on') == 'load') {
        fireICRequest(elt);
      } else if ($(elt).attr('ic-trigger-on') == 'scrolled-into-view') {
        initScrollHandler();
        setTimeout(function () {
          $(window).trigger('scroll');
        }, 100); // Trigger a scroll in case element is already viewable
      } else {
        var triggerOn = $(elt).attr('ic-trigger-on').split(" ");
        $(elt).on(eventFor(triggerOn[0], $(elt)), function (e) {

          var onBeforeTrigger = closestAttrValue(elt, 'ic-on-beforeTrigger');
          if (onBeforeTrigger) {
            if (globalEval('(function (evt, elt) {' + onBeforeTrigger + '})')(e, $(elt)) == false) {
              log($(elt), "ic-trigger cancelled by ic-on-beforeTrigger", "DEBUG");
              return false;
            }
          }

          if (triggerOn[1] == 'changed') {
            var currentVal = $(elt).val();
            var previousVal = $(elt).data('ic-previous-val');
            $(elt).data('ic-previous-val', currentVal);
            if (currentVal != previousVal) {
              fireICRequest($(elt));
            }
          } else {
            fireICRequest($(elt));
          }
          if (preventDefault(elt)) {
            e.preventDefault();
            return false;
          }
          return true;
        });
      }
    }
  }

  //============================================================----
  // Macro support
  //============================================================----

  function processMacro(macro, elt) {
    // action attributes
    if (macro == 'ic-post-to') {
      setIfAbsent(elt, 'ic-src', elt.attr('ic-post-to'));
      setIfAbsent(elt, 'ic-verb', 'POST');
      setIfAbsent(elt, 'ic-trigger-on', 'default');
      setIfAbsent(elt, 'ic-deps', 'ignore');
    }
    if (macro == 'ic-put-to') {
      setIfAbsent(elt, 'ic-src', elt.attr('ic-put-to'));
      setIfAbsent(elt, 'ic-verb', 'PUT');
      setIfAbsent(elt, 'ic-trigger-on', 'default');
      setIfAbsent(elt, 'ic-deps', 'ignore');
    }
    if (macro == 'ic-patch-to') {
      setIfAbsent(elt, 'ic-src', elt.attr('ic-patch-to'));
      setIfAbsent(elt, 'ic-verb', 'PATCH');
      setIfAbsent(elt, 'ic-trigger-on', 'default');
      setIfAbsent(elt, 'ic-deps', 'ignore');
    }
    if (macro == 'ic-get-from') {
      setIfAbsent(elt, 'ic-src', elt.attr('ic-get-from'));
      setIfAbsent(elt, 'ic-trigger-on', 'default');
      setIfAbsent(elt, 'ic-deps', 'ignore');
    }
    if (macro == 'ic-delete-from') {
      setIfAbsent(elt, 'ic-src', elt.attr('ic-delete-from'));
      setIfAbsent(elt, 'ic-verb', 'DELETE');
      setIfAbsent(elt, 'ic-trigger-on', 'default');
      setIfAbsent(elt, 'ic-deps', 'ignore');
    }
    // non-action attributes
    var value = null;
    var url = null;
    if (macro == 'ic-style-src') {
      value = elt.attr('ic-style-src').split(":");
      var styleAttribute = value[0];
      url = value[1];
      setIfAbsent(elt, 'ic-src', url);
      setIfAbsent(elt, 'ic-target', 'this.style.' + styleAttribute);
    }
    if (macro == 'ic-attr-src') {
      value = elt.attr('ic-attr-src').split(":");
      var attribute = value[0];
      url = value[1];
      setIfAbsent(elt, 'ic-src', url);
      setIfAbsent(elt, 'ic-target', 'this.' + attribute);
    }
    if (macro == 'ic-prepend-from') {
      setIfAbsent(elt, 'ic-src', elt.attr('ic-prepend-from'));
    }
    if (macro == 'ic-append-from') {
      setIfAbsent(elt, 'ic-src', elt.attr('ic-append-from'));
    }
  }

  function setIfAbsent(elt, attr, value) {
    if (elt.attr(attr) == null) {
      elt.attr(attr, value);
    }
  }

  //============================================================----
  // Utilities
  //============================================================----

  function isScrolledIntoView(elem) {
    var docViewTop = $(window).scrollTop();
    var docViewBottom = docViewTop + $(window).height();

    var elemTop = $(elem).offset().top;
    var elemBottom = elemTop + $(elem).height();

    return ((elemBottom >= docViewTop) && (elemTop <= docViewBottom)
    && (elemBottom <= docViewBottom) && (elemTop >= docViewTop));
  }

  function maybeScrollToTarget(elt, target) {
    if (closestAttrValue(elt, 'ic-scroll-to-target') != "false" &&
      (closestAttrValue(elt, 'ic-scroll-to-target') == 'true' ||
      closestAttrValue(target, 'ic-scroll-to-target') == 'true')) {
      var offset = -50; // -50 px default offset padding
      if (closestAttrValue(elt, 'ic-scroll-offset')) {
        offset = parseInt(closestAttrValue(elt, 'ic-scroll-offset'));
      } else if (closestAttrValue(target, 'ic-scroll-offset')) {
        offset = parseInt(closestAttrValue(target, 'ic-scroll-offset'));
      }
      var currentPosition = target.offset().top;
      var portalTop = $(window).scrollTop();
      var portalEnd = portalTop + window.innerHeight;
      //if the current top of this element is not visible, scroll it to the top position
      if (currentPosition < portalTop || currentPosition > portalEnd) {
        offset += currentPosition;
        $('html,body').animate({scrollTop: offset}, 400);
      }
    }
  }

  function getTransitionDuration(elt, target) {
    var transitionDuration = closestAttrValue(elt, 'ic-transition-duration');
    if (transitionDuration) {
      return parseInterval(transitionDuration);
    }
    transitionDuration = closestAttrValue(target, 'ic-transition-duration');
    if (transitionDuration) {
      return parseInterval(transitionDuration);
    }
    var duration = 0;
    var durationStr = $(target).css('transition-duration');
    if(durationStr) {
      duration += parseInterval(durationStr);
    }
    var delayStr = $(target).css('transition-delay');
    if(delayStr) {
      duration += parseInterval(delayStr);
    }
    return duration;
  }

  function processICResponse(responseContent, elt, forHistory) {
    if (responseContent && responseContent != "" && responseContent != " ") {

      log(elt, "response content: \n" + responseContent, "DEBUG");
      var target = getTarget(elt);

      var contentToSwap = maybeFilter(responseContent, closestAttrValue(elt, 'ic-select-from-response'));

      var doSwap = function () {
        if (closestAttrValue(elt, 'ic-replace-target') == "true") {
          try {
            target.replaceWith(contentToSwap);
          } catch (e) {
            log(elt, formatError(e), "ERROR");
          }
          processNodes(contentToSwap);
          fireReadyStuff($(target));
        } else {
          if (elt.is('[ic-prepend-from]')) {
            prepend(target, contentToSwap);
            processNodes(contentToSwap);
            fireReadyStuff($(target));
          } else if (elt.is('[ic-append-from]')) {
            append(target, contentToSwap);
            processNodes(contentToSwap);
            fireReadyStuff($(target));
          } else {
            try {
              target.empty().append(contentToSwap);
            } catch (e) {
              log(elt, formatError(e), "ERROR");
            }
            $(target).children().each(function () {
              processNodes($(this));
            });
            fireReadyStuff($(target));
          }
          updateIntercoolerMetaData(target);
          if(forHistory != true) {
            maybeScrollToTarget(elt, target);
          }
        }
      };

      if(target.length == 0) {
        //TODO cgross - refactor getTarget to return printable string here
        log(elt, "Invalid target for element: " + $(elt).closest('[ic-target]').attr('ic-target'), "ERROR");
        return;
      }

      var delay = getTransitionDuration(elt, target);
      target.addClass('ic-transitioning');
      setTimeout(function () {
        doSwap();
        setTimeout(function () {
          target.removeClass('ic-transitioning');
          _history.updateHistory();
        }, 20);
      }, delay);
    } else {
      log(elt, "Empty response, nothing to do here.", "DEBUG");
    }
  }

  function maybeFilter(newContent, filter) {
    var content = $.parseHTML(newContent, null, true);
    var asQuery = $(content);
    if (filter) {
      return asQuery.filter(filter).add(asQuery.find(filter)).contents();
    } else {
      return asQuery;
    }
  }

  function getStyleTarget(elt) {
    var val = closestAttrValue(elt, 'ic-target');
    if (val && val.indexOf("this.style.") == 0) {
      return val.substr(11)
    } else {
      return null;
    }
  }

  function getAttrTarget(elt) {
    var val = closestAttrValue(elt, 'ic-target');
    if (val && val.indexOf("this.") == 0) {
      return val.substr(5)
    } else {
      return null;
    }
  }

  function fireICRequest(elt, alternateHandler) {

    var triggerOrigin = elt;
    if (!elt.is('[ic-src]')) {
      elt = elt.closest('[ic-src]');
    }

    var confirmText = closestAttrValue(elt, 'ic-confirm');
    if (confirmText) {
      if (!confirm(confirmText)) {
        return;
      }
    }

    if (elt.length > 0) {
      var icEventId = uuid();
      elt.data('ic-event-id', icEventId);
      var invokeRequest = function () {

        // if an existing request is in flight for this element, push this request as the next to be executed
        if (elt.data('ic-request-in-flight') == true) {
          elt.data('ic-next-request', invokeRequest);
          return;
        }

        if (elt.data('ic-event-id') == icEventId) {
          var styleTarget = getStyleTarget(elt);
          var attrTarget = styleTarget ? null : getAttrTarget(elt);
          var verb = verbFor(elt);

          var success = alternateHandler || function (data) {
              if (styleTarget) {
                elt.css(styleTarget, data);
              } else if (attrTarget) {
                elt.attr(attrTarget, data);
              } else {
                processICResponse(data, elt);
                if (verb != 'GET') {
                  refreshDependencies(elt.attr('ic-src'), elt);
                }
              }
            };

          handleRemoteRequest(elt, verb, elt.attr('ic-src'), getParametersForElement(verb, elt, triggerOrigin), success);
        }
      };

      var triggerDelay = closestAttrValue(elt, 'ic-trigger-delay');
      if (triggerDelay) {
        setTimeout(invokeRequest, parseInterval(triggerDelay));
      } else {
        invokeRequest();
      }
    }
  }

  //============================================================
  // History Support
  //============================================================

  function newIntercoolerHistory(storage, history, slotLimit, historyVersion) {

    /* Constants */
    var HISTORY_SUPPORT_SLOT = 'ic-history-support';
    var HISTORY_SLOT_PREFIX = "ic-hist-elt-";

    /* Instance Vars */
    var historySupportData = JSON.parse(storage.getItem(HISTORY_SUPPORT_SLOT));
    var _snapshot = null;

    // Reset history if the history config has changed
    if (historyConfigHasChanged(historySupportData)) {
      log(getTargetForHistory($('body')), "Intercooler History configuration changed, clearing history", "INFO");
      clearHistory();
    }

    if(historySupportData == null) {
      historySupportData = {
        slotLimit : slotLimit,
        historyVersion: historyVersion,
        lruList: []
      };
    }

    /* Instance Methods  */
    function historyConfigHasChanged(historySupportData) {
      return historySupportData == null ||
        historySupportData.slotLimit != slotLimit ||
        historySupportData.historyVersion != historyVersion ||
        historySupportData.lruList == null
    }

    function clearHistory () {
      var keys = [];
      for (var i = 0; i < storage.length; i++) {
        if (storage.key(i).indexOf(HISTORY_SLOT_PREFIX) == 0) {
          keys.push(storage.key(i));
        }
      }
      for (var j = 0; j < keys.length; j++) {
        storage.removeItem(keys[j]);
      }
      storage.removeItem(HISTORY_SUPPORT_SLOT);
      historySupportData = {
        slotLimit : slotLimit,
        historyVersion: historyVersion,
        lruList: []
      };
    }

    function updateLRUList(url) {
      var lruList = historySupportData.lruList;
      var currentIndex = lruList.indexOf(url);
      var t = getTargetForHistory($('body'));
      // found in current list, shift it to the end
      if(currentIndex >= 0) {
        log(t, "URL found in LRU list, moving to end", "INFO");
        lruList.splice(currentIndex, 1);
        lruList.push(url);
      } else {
        // not found, add and shift if necessary
        log(t, "URL not found in LRU list, adding", "INFO");
        lruList.push(url);
        if(lruList.length > historySupportData.slotLimit) {
          var urlToDelete = lruList.shift();
          log(t, "History overflow, removing local history for " + urlToDelete, "INFO");
          storage.removeItem(HISTORY_SLOT_PREFIX + urlToDelete);
        }
      }

      // save history metadata
      storage.setItem(HISTORY_SUPPORT_SLOT, JSON.stringify(historySupportData));
      return lruList;
    }

    function saveHistoryData(restorationData) {
      var content = JSON.stringify(restorationData);
      try {
        storage.setItem(restorationData.id, content);
      } catch (e) {
        //quota error, nuke local cache
        try {
          clearHistory();
          storage.setItem(restorationData.id, content);
        } catch (e) {
          log(getTargetForHistory($('body')), "Unable to save intercooler history with entire history cleared, is something else eating " +
            "local storage? History Limit:" + slotLimit, "ERROR");
        }
      }
    }

    function makeHistoryEntry(html, yOffset, url) {
      var restorationData = {
        "url": url,
        "id": HISTORY_SLOT_PREFIX + url,
        "content": html,
        "yOffset": yOffset,
        "timestamp": new Date().getTime()
      };
      updateLRUList(url);
      // save to the history slot
      saveHistoryData(restorationData);
      return restorationData;
    }

    function addPopStateHandler(windowToAdd) {
      if (windowToAdd.onpopstate == null || windowToAdd.onpopstate['ic-on-pop-state-handler'] != true) {
        var currentOnPopState = windowToAdd.onpopstate;
        windowToAdd.onpopstate = function (event) {
          getTargetForHistory($('body')).trigger('handle.onpopstate.ic');
          if (!handleHistoryNavigation(event)) {
            if (currentOnPopState) {
              currentOnPopState(event);
            }
          }
          getTargetForHistory($('body')).trigger('pageLoad.ic');
        };
        windowToAdd.onpopstate['ic-on-pop-state-handler'] = true;
      }
    }

    function updateHistory() {
      if(_snapshot) {
        pushUrl(_snapshot.newUrl, currentUrl(), _snapshot.oldHtml, _snapshot.yOffset);
        _snapshot = null;
      }
    }

    function pushUrl(newUrl, originalUrl, originalHtml, yOffset) {

      var historyEntry = makeHistoryEntry(originalHtml, yOffset, originalUrl);
      history.replaceState({"ic-id": historyEntry.id}, "", "");

      var t = getTargetForHistory($('body'));
      var restorationData = makeHistoryEntry(t.html(), window.pageYOffset, newUrl);
      history.pushState({'ic-id': restorationData.id}, "", newUrl);

      t.trigger("pushUrl.ic", [t, restorationData]);
    }

    function handleHistoryNavigation(event) {
      var data = event.state;
      if (data && data['ic-id']) {
        var historyData = JSON.parse(storage.getItem(data['ic-id']));
        if (historyData) {
          processICResponse(historyData["content"], getTargetForHistory($('body')), true);
          if(historyData["yOffset"]) {
            window.scrollTo(0, historyData["yOffset"])
          }
          return true;
        } else {
          $.get(currentUrl(), {'ic-restore-history' : true}, function(data, status){
            var newDoc = createDocument(data);
            var replacementHtml = getTargetForHistory(newDoc).html();
            processICResponse(replacementHtml, getTargetForHistory($('body')), true);
          });
        }
      }
      return false;
    }

    function getTargetForHistory(elt) {
      var explicitHistoryTarget = elt.find('[ic-history-elt]');
      if (explicitHistoryTarget.length > 0) {
        return explicitHistoryTarget;
      } else {
        return elt;
      }
    }

    function snapshotForHistory(newUrl) {
      var t = getTargetForHistory($('body'));
      t.trigger("beforeHistorySnapshot.ic", [t]);
      _snapshot = {
        newUrl: newUrl,
        oldHtml: t.html(),
        yOffset :  window.pageYOffset
      }
    }

    function dumpLocalStorage() {
      var str = "";
      var keys = [];
      for (var x in storage) {
        keys.push(x);
      }
      keys.sort();
      var total = 0;
      for (var i in keys) {
        var size = (storage[keys[i]].length * 2);
        total += size;
        str += keys[i] + "=" + (size / 1024 / 1024).toFixed(2) + " MB\n";
      }
      return str + "\nTOTAL LOCAL STORAGE: " + (total / 1024 / 1024).toFixed(2) + " MB";
    }

    function supportData() {
      return historySupportData;
    }

    /* API */
    return {
      clearHistory : clearHistory,
      updateHistory : updateHistory,
      addPopStateHandler: addPopStateHandler,
      snapshotForHistory: snapshotForHistory,
      _internal : {
        addPopStateHandler: addPopStateHandler,
        supportData: supportData,
        dumpLocalStorage: dumpLocalStorage,
        updateLRUList: updateLRUList
      }
    }
  }

  function getSlotLimit() {
    return 20;
  }

  function refresh(val) {
    if (typeof val == 'string' || val instanceof String) {
      refreshDependencies(val);
    } else {
      fireICRequest(val);
    }
    return Intercooler;
  }

  var _history = newIntercoolerHistory(localStorage, window.history, getSlotLimit(), .1);

  //============================================================
  // Local references transport
  //============================================================

  $.ajaxTransport("text", function (options, origOptions) {
      if (origOptions.url[0] == "#") {
        var ltAttr = "ic-local-";
        var src = $(origOptions.url);
        var rsphdr = [];
        var status = 200;
        var statusText = "OK";
        src.each(function (i, el) {
          $.each(el.attributes, function (j, attr) {
            if (attr.name.substr(0, ltAttr.length) == ltAttr) {
              var lhName = attr.name.substring(ltAttr.length);
              if (lhName == "status") {
                var statusLine = attr.value.match(/(\d+)\s?(.*)/);
                if (statusLine != null) {
                  status = statusLine[1];
                  statusText = statusLine[2];
                } else {
                  status = "500";
                  statusText = "Attribute Error";
                }
              } else {
                rsphdr.push(lhName + ": " + attr.value);
              }
            }
          });
        });
        var rsp = src.length > 0 ? src.html() : "";
        return {
          send: function (reqhdr, completeCallback) {
            completeCallback(status, statusText, {html: rsp}, rsphdr.join("\n"));
          },
          abort: function () {
          }
        }
      } else {
        return null;
      }
    }
  );

  //============================================================
  // Bootstrap
  //============================================================

    function init() {
      var elt = $('body');
      processNodes(elt);
      fireReadyStuff(elt);
      _history.addPopStateHandler(window);
      if (location.search && location.search.indexOf("ic-launch-debugger=true") >= 0) {
        Intercooler.debug();
      }
    }

    $(function () {
      init();
    });

  /* ===================================================
   * API
   * =================================================== */
  return {
    refresh: refresh,
    history: _history,
    triggerRequest: fireICRequest,
    processNodes: processNodes,
    closestAttrValue: closestAttrValue,
    verbFor: verbFor,
    isDependent: isDependent,
    getTarget: getTarget,
    ready : function(readyHandler) {
      _readyHandlers.push(readyHandler);
    },
    debug: function () {
      var debuggerUrl = closestAttrValue('body', 'ic-debugger-url') ||
        "https://intercoolerreleases-leaddynocom.netdna-ssl.com/intercooler-debugger.js";
      $.getScript(debuggerUrl)
        .fail(function (jqxhr, settings, exception) {
          log($('body'), formatError(exception), "ERROR");
        });
    },
    _internal : {
      init:init
    },
    /* ===================================================
     * Deprecated API
     * =================================================== */
    defaultTransition: function () {
      log($('body'), "This method is no longer supported.  Transitions are now done via CSS", "ERROR");
    },
    defineTransition: function () {
      log($('body'), "This method is no longer supported.  Transitions are now done via CSS", "ERROR");
    },
    addURLHandler: function () {
      log($('body'), "This method is no longer supported.  Please use the jQuery mockjax plugin instead: https://github.com/jakerella/jquery-mockjax", "ERROR");
    },
    setRemote: function () {
      log($('body'), "This method is no longer supported.  Please use the jQuery mockjax plugin instead: https://github.com/jakerella/jquery-mockjax", "ERROR");
    }
  }
})();
